Цель исследования:
Изучить особенности поведения пользователей приложения
Изучить воронку продаж
Исследовать результаты A/A/B-эксперимента, где А - контрольные группы, В - тестовая группа, для которой поменяли шрифты в приложении.
Ход исследования:
1.1. Изменить названия столбцов
1.2. Заменить тип данных в столбце с датой и временем
1.3. Добавить столбец с датой
1.4. Дать группам названия вместо номеров
1.5. Проверить данные на дубликаты и пропуски, очистить данные
1.6. Проверить группы на пересечения
1.7. Сделать вывод по разделу
2.1. Вывести количество событий
2.2. Вывести количество пользователей
2.3. Вывести количество пользователей в каждой группе
2.4. Вывести среднее количество событий на пользователя
2.5. Вывести среднее количество событий на пользователя в разрезе групп
2.6. Вывести медианное количество событий на пользователя
2.7. Вывести медианное количество событий на пользователя в разрезе групп
2.8. Вывести максимальную и минимальную дату событий
2.9. Визуализировать информацию о количестве событий на каждую дату
2.10. Скорректировать временной промежуток
2.11. Проверить потери в данных
2.12 Проверить структуру оставшихся данных
2.13. Сформировать таблицу в которой соберем для каждого пользователя количество каждого вида действий.
2.14. Сделать вывод по разделу
3.1. Вывести частоту встречаемости каждого события
3.2. Рассмотреть частому встечаемости событий по пользователям
3.3. Определить события, формирующие воронку и их очередность
3.4. Рассчитать коэффициенты удержания для каждого шага
3.5. Рассчитать удержание для пользователей которые начали свой путь на сайте с первого этапа
3.6. Сделать вывод по разделу
4.1. Сформировать таблицу с количеством пользователей в каждой группе
4.2. Сформировать таблицу с количеством и процентным соотношением пользователей совершивших каждое действие по группам
4.3. Сформировать гипотезы для тестов
4.4. Проверить, есть ли отличия между двумя контрольными группами на каждом шаге
4.5. Проверить, есть ли отличия между первой контрольной и тестовой группами на каждом шаге
4.6. Проверить, есть ли отличия между второй контрольной и тестовой группами на каждом шаге
4.7. Проверить, есть ли отличия между объединенными контрольными и тестовой группами на каждом шаге
4.8. Сделать вывод по разделу
Импорт библиотек и функций:
from scipy import stats as st
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import datetime as dt
import seaborn as sns
import math as mth
import plotly.express as px
def load(df):
print ('Первые 10 строк таблицы:')
print ()
display (df.head(10))
print ()
print ('Иноформация о таблице:')
print ()
display (df.info())
print ()
print ('Количество дубликатов в таблице:')
print ()
display (df.duplicated().sum())
print ()
print ('Количество пропусков в таблице:')
print ()
display (df.isna().sum())
print ()
print ('Процентное соотношение пропусков к общему числу значений для каждого столбца')
print ()
display (pd.DataFrame(round(df.isna().mean()*100,)).style.background_gradient('coolwarm'))
pd.set_option('display.max_columns', None)
pd.set_option('display.max_colwidth', None)
pd.options.mode.chained_assignment = None # default='warn'
df = pd.read_csv('/datasets/logs_exp.csv', sep='\t')
load(df)
Первые 10 строк таблицы:
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
| 5 | CartScreenAppear | 6217807653094995999 | 1564055323 | 248 |
| 6 | OffersScreenAppear | 8351860793733343758 | 1564066242 | 246 |
| 7 | MainScreenAppear | 5682100281902512875 | 1564085677 | 246 |
| 8 | MainScreenAppear | 1850981295691852772 | 1564086702 | 247 |
| 9 | MainScreenAppear | 5407636962369102641 | 1564112112 | 246 |
Иноформация о таблице: <class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
None
Количество дубликатов в таблице:
413
Количество пропусков в таблице:
EventName 0 DeviceIDHash 0 EventTimestamp 0 ExpId 0 dtype: int64
Процентное соотношение пропусков к общему числу значений для каждого столбца
| 0 | |
|---|---|
| EventName | 0.000000 |
| DeviceIDHash | 0.000000 |
| EventTimestamp | 0.000000 |
| ExpId | 0.000000 |
df = df.rename(columns={
'EventName' : 'event_name',
'DeviceIDHash' : 'user_id',
'EventTimestamp' : 'date_time',
'ExpId' : 'group'})
df['date_time'] = pd.to_datetime(df['date_time'],unit='s')
df['date'] = df['date_time'].dt.date
def rename_group(group):
if group==246:
return 'A1'
elif group==247:
return 'A2'
elif group==248:
return 'B'
df['group'] = df['group'].apply(rename_group)
df = df.drop_duplicates()
load(df)
Первые 10 строк таблицы:
| event_name | user_id | date_time | group | date | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | A1 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | A1 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | B | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | B | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | B | 2019-07-25 |
| 5 | CartScreenAppear | 6217807653094995999 | 2019-07-25 11:48:43 | B | 2019-07-25 |
| 6 | OffersScreenAppear | 8351860793733343758 | 2019-07-25 14:50:42 | A1 | 2019-07-25 |
| 7 | MainScreenAppear | 5682100281902512875 | 2019-07-25 20:14:37 | A1 | 2019-07-25 |
| 8 | MainScreenAppear | 1850981295691852772 | 2019-07-25 20:31:42 | A2 | 2019-07-25 |
| 9 | MainScreenAppear | 5407636962369102641 | 2019-07-26 03:35:12 | A1 | 2019-07-26 |
Иноформация о таблице: <class 'pandas.core.frame.DataFrame'> Int64Index: 243713 entries, 0 to 244125 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 243713 non-null object 1 user_id 243713 non-null int64 2 date_time 243713 non-null datetime64[ns] 3 group 243713 non-null object 4 date 243713 non-null object dtypes: datetime64[ns](1), int64(1), object(3) memory usage: 11.2+ MB
None
Количество дубликатов в таблице:
0
Количество пропусков в таблице:
event_name 0 user_id 0 date_time 0 group 0 date 0 dtype: int64
Процентное соотношение пропусков к общему числу значений для каждого столбца
| 0 | |
|---|---|
| event_name | 0.000000 |
| user_id | 0.000000 |
| date_time | 0.000000 |
| group | 0.000000 |
| date | 0.000000 |
users_a1 = df.query('group == "A1"')[['user_id', 'group']]
users_a2 = df.query('group == "A2"')[['user_id', 'group']]
users_b = df.query('group == "B"')[['user_id', 'group']]
users_a1_a2 = users_a1.merge(users_a2, on='user_id', how='inner')
print('Количество пользователей попавших в в группы A1 и A2:', users_a1_a2['user_id'].nunique())
users_a1_b = users_a1.merge(users_b, on='user_id', how='inner')
print('Количество пользователей попавших в группы A1 и B:', users_a1_b['user_id'].nunique())
users_a2_b = users_a2.merge(users_b, on='user_id', how='inner')
print('Количество пользователей попавших в группы A2 и B:', users_a2_b['user_id'].nunique())
Количество пользователей попавших в в группы A1 и A2: 0 Количество пользователей попавших в группы A1 и B: 0 Количество пользователей попавших в группы A2 и B: 0
Пересечения пользователей в группах отсутствуют.
В таблице df пять столбцов :
event_name - название события
user_id - идентификатор пользователя
date_time - дата и время совершения действия
group - группа эксперимента
date - дата совершения действия (без времени)
Таблица содержит данные о 243713 действиях на сайте.
Пропусков в таблице не было, а 413 строк-дубликатов удалены.
Изменены названия столбцов, названия групп эксперимета, заменен тип данных в столбце с датой и временем.
print(df['event_name'].unique())
print()
print('Всего событий в логе:',df['event_name'].nunique())
['MainScreenAppear' 'PaymentScreenSuccessful' 'CartScreenAppear' 'OffersScreenAppear' 'Tutorial'] Всего событий в логе: 5
print('Всего пользователей в логе:',df['user_id'].nunique())
Всего пользователей в логе: 7551
df.groupby('group').agg({'user_id':'nunique'})
| user_id | |
|---|---|
| group | |
| A1 | 2489 |
| A2 | 2520 |
| B | 2542 |
Есть три исследуемые группы, 246 и 247 контрольные, 248 - тестовая, в них примерно одинаковое количество пользователей.
(df.groupby('user_id').agg({'event_name':'count'})).mean()
event_name 32.275593 dtype: float64
df_A1 = df.query('group == "A1"')
df_A2 = df.query('group == "A2"')
df_B = df.query('group == "B"')
print('Среднее количество событий на пользователя группы A1:', (df_A1.groupby('user_id').agg({'event_name':'count'})).mean())
print()
print('Среднее количество событий на пользователя группы A2:', (df_A2.groupby('user_id').agg({'event_name':'count'})).mean())
print()
print('Среднее количество событий на пользователя группы B:', (df_B.groupby('user_id').agg({'event_name':'count'})).mean())
Среднее количество событий на пользователя группы A1: event_name 32.214142 dtype: float64 Среднее количество событий на пользователя группы A2: event_name 30.93254 dtype: float64 Среднее количество событий на пользователя группы B: event_name 33.667191 dtype: float64
Среднее количество событий на пользователя 32, но это значение может быть искажено выбросами, чтобы это проверить построим диаграмму.
df_event_count = df.groupby('user_id').agg({'event_name':'count'})
df_event_count= df_event_count.reset_index()
df_event_count
x_values = pd.Series(range(0, len(df_event_count)))
plt.figure(figsize=(17, 7))
plt.scatter(x_values, df_event_count['event_name'],lw=5)
plt.title('Точечная диаграмма количества событий на пользователя', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('Количество действий пользователя', fontsize=14);
Как и предполагалось, в данных есть выбросы больше 2000 действий на пользователя, они сильно искажают среднее, так что для оценки активности среднестатистического пользователя лучше использовать медианное значение.
(df.groupby('user_id').agg({'event_name':'count'})).median()
event_name 20.0 dtype: float64
print('Медианное количество событий на пользователя группы A1',
(df_A1.groupby('user_id').agg({'event_name':'count'})).median())
print()
print('Медианное количество событий на пользователя группы A2',
(df_A2.groupby('user_id').agg({'event_name':'count'})).median())
print()
print('Медианное количество событий на пользователя группы B',
(df_B.groupby('user_id').agg({'event_name':'count'})).median())
Медианное количество событий на пользователя группы A1 event_name 19.0 dtype: float64 Медианное количество событий на пользователя группы A2 event_name 19.5 dtype: float64 Медианное количество событий на пользователя группы B event_name 20.0 dtype: float64
print('Минимальная дата для группы A1:', df_A1['date'].min())
print('Максимальная дата для группы A1:', df_A1['date'].max())
print()
print('Минимальная дата для группы A2:', df_A2['date'].min())
print('Максимальная дата для группы A2:', df_A2['date'].max())
print()
print('Минимальная дата для группы B:', df_B['date'].min())
print('Максимальная дата для группы B:', df_B['date'].max())
Минимальная дата для группы A1: 2019-07-25 Максимальная дата для группы A1: 2019-08-07 Минимальная дата для группы A2: 2019-07-25 Максимальная дата для группы A2: 2019-08-07 Минимальная дата для группы B: 2019-07-25 Максимальная дата для группы B: 2019-08-07
Минимальная и максимальная даты для каждой группы совпадают.
df_grouped = df.groupby(['date', 'group'])['event_name'].count()
df_grouped = df_grouped.reset_index()
order = df_grouped['date'].drop_duplicates()
sns.catplot(y="event_name",
x="date",
hue="group",
order=order,
data=df_grouped,
kind="bar",
height=7, aspect=2.5, legend=False)
plt.title('Количество событий по датам(по группам)', fontsize=18)
plt.ylabel('Количество событий', fontsize=16)
plt.xlabel('Дата', fontsize=16)
plt.grid()
plt.legend(fontsize=14, loc='upper left')
plt.xticks(rotation=45, fontsize=14)
plt.yticks(fontsize=14);
Данных за последнюю неделю июля в разы меньше чем за начало августа. Похоже данные начали собирать именно с начала августа, так что июльские можно отбросить.
df_clear = df.loc[df['date_time'] >= '2019-08-01 00:00:00'].reset_index(drop=True)
df_clear.sort_values(by='date_time')
| event_name | user_id | date_time | group | date | |
|---|---|---|---|---|---|
| 0 | Tutorial | 3737462046622621720 | 2019-08-01 00:07:28 | A1 | 2019-08-01 |
| 1 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:00 | A1 | 2019-08-01 |
| 2 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:55 | A1 | 2019-08-01 |
| 3 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:08:58 | A1 | 2019-08-01 |
| 4 | MainScreenAppear | 1433840883824088890 | 2019-08-01 00:08:59 | A2 | 2019-08-01 |
| ... | ... | ... | ... | ... | ... |
| 240882 | MainScreenAppear | 4599628364049201812 | 2019-08-07 21:12:25 | A2 | 2019-08-07 |
| 240883 | MainScreenAppear | 5849806612437486590 | 2019-08-07 21:13:59 | A1 | 2019-08-07 |
| 240884 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:43 | A1 | 2019-08-07 |
| 240885 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:58 | A1 | 2019-08-07 |
| 240886 | OffersScreenAppear | 5746969938801999050 | 2019-08-07 21:15:17 | A1 | 2019-08-07 |
240887 rows × 5 columns
print('Минимальная дата:', df_clear['date'].min())
print('Максимальная дата:', df_clear['date'].max())
Минимальная дата: 2019-08-01 Максимальная дата: 2019-08-07
load(df_clear)
Первые 10 строк таблицы:
| event_name | user_id | date_time | group | date | |
|---|---|---|---|---|---|
| 0 | Tutorial | 3737462046622621720 | 2019-08-01 00:07:28 | A1 | 2019-08-01 |
| 1 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:00 | A1 | 2019-08-01 |
| 2 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:55 | A1 | 2019-08-01 |
| 3 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:08:58 | A1 | 2019-08-01 |
| 4 | MainScreenAppear | 1433840883824088890 | 2019-08-01 00:08:59 | A2 | 2019-08-01 |
| 5 | MainScreenAppear | 4899590676214355127 | 2019-08-01 00:10:15 | A2 | 2019-08-01 |
| 6 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:10:26 | A1 | 2019-08-01 |
| 7 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:10:47 | A1 | 2019-08-01 |
| 8 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:11:10 | A1 | 2019-08-01 |
| 9 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:11:20 | A1 | 2019-08-01 |
Иноформация о таблице: <class 'pandas.core.frame.DataFrame'> RangeIndex: 240887 entries, 0 to 240886 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 240887 non-null object 1 user_id 240887 non-null int64 2 date_time 240887 non-null datetime64[ns] 3 group 240887 non-null object 4 date 240887 non-null object dtypes: datetime64[ns](1), int64(1), object(3) memory usage: 9.2+ MB
None
Количество дубликатов в таблице:
0
Количество пропусков в таблице:
event_name 0 user_id 0 date_time 0 group 0 date 0 dtype: int64
Процентное соотношение пропусков к общему числу значений для каждого столбца
| 0 | |
|---|---|
| event_name | 0.000000 |
| user_id | 0.000000 |
| date_time | 0.000000 |
| group | 0.000000 |
| date | 0.000000 |
print('Количество записей исходной таблицы:',
len(df))
print('Количество записей очищенной таблицы:',
len(df_clear))
print('Количество удаленных записей:',
len(df)-len(df_clear))
print('Процент удаленных записей:',
round((len(df)-len(df_clear))/len(df), 3), '%')
print ()
print('Количество пользователей в исходной таблице:',
df['user_id'].nunique())
print('Количество пользователей в очищенной таблице:',
df_clear['user_id'].nunique())
print('Количество удаленных пользователей:',
df['user_id'].nunique()-df_clear['user_id'].nunique())
print('Процент удаленных пользователей:',
round((df['user_id'].nunique()-df_clear['user_id'].nunique())/df['user_id'].nunique(), 3), '%')
Количество записей исходной таблицы: 243713 Количество записей очищенной таблицы: 240887 Количество удаленных записей: 2826 Процент удаленных записей: 0.012 % Количество пользователей в исходной таблице: 7551 Количество пользователей в очищенной таблице: 7534 Количество удаленных пользователей: 17 Процент удаленных пользователей: 0.002 %
print('Количество пользователей группы A1:',
df_clear.query('group == "A1"')['user_id'].nunique())
print('Отношение количества пользователей группы A1 к общему числу пользователей:',
round(df_clear.query('group == "A1"')['user_id'].nunique()/df_clear['user_id'].nunique(),2))
print()
print('Количество пользователей группы A2:',
df_clear.query('group == "A2"')['user_id'].nunique())
print('Отношение количества пользователей группы A2 к общему числу пользователей:',
round(df_clear.query('group == "A2"')['user_id'].nunique()/df_clear['user_id'].nunique(),2))
print()
print('Количество пользователей группы B:',
df_clear.query('group == "B"')['user_id'].nunique())
print('Отношение количества пользователей группы B к общему числу пользователей:',
round(df_clear.query('group == "B"')['user_id'].nunique()/df_clear['user_id'].nunique(),2))
Количество пользователей группы A1: 2484 Отношение количества пользователей группы A1 к общему числу пользователей: 0.33 Количество пользователей группы A2: 2513 Отношение количества пользователей группы A2 к общему числу пользователей: 0.33 Количество пользователей группы B: 2537 Отношение количества пользователей группы B к общему числу пользователей: 0.34
Хоть мы и удалили половину исследуемого периода, потери в данных оказались незначительны. Распределение данных по группам эксперимента тоже не пострадало.
df_pivot_events = df_clear[df_clear['event_name'] != 'Tutorial'].pivot_table(
index=['user_id', 'group'],
columns= 'event_name',
values='date_time',
aggfunc= 'count')
df_pivot_events = df_pivot_events.reset_index()
df_pivot_events.columns=[
'user_id',
'group',
'count_cart', 'count_main', 'count_offers', 'count_payment']
df_pivot_events = df_pivot_events[[
'user_id',
'group',
'count_main',
'count_offers',
'count_cart',
'count_payment']]
df_pivot_events[[
'count_main',
'count_offers',
'count_cart',
'count_payment']] = df_pivot_events[[
'count_main',
'count_offers',
'count_cart',
'count_payment']].fillna(value=0)
df_pivot_events.head(20)
| user_id | group | count_main | count_offers | count_cart | count_payment | |
|---|---|---|---|---|---|---|
| 0 | 6888746892508752 | A1 | 1.0 | 0.0 | 0.0 | 0.0 |
| 1 | 6909561520679493 | A2 | 2.0 | 1.0 | 1.0 | 1.0 |
| 2 | 6922444491712477 | A1 | 19.0 | 12.0 | 8.0 | 8.0 |
| 3 | 7435777799948366 | B | 6.0 | 0.0 | 0.0 | 0.0 |
| 4 | 7702139951469979 | A2 | 40.0 | 87.0 | 5.0 | 5.0 |
| 5 | 8486814028069281 | B | 4.0 | 2.0 | 4.0 | 0.0 |
| 6 | 8740973466195562 | A1 | 8.0 | 1.0 | 0.0 | 0.0 |
| 7 | 9841258664663090 | B | 10.0 | 8.0 | 8.0 | 2.0 |
| 8 | 12692216027168046 | A1 | 7.0 | 3.0 | 0.0 | 0.0 |
| 9 | 15708180189885246 | A1 | 27.0 | 38.0 | 38.0 | 23.0 |
| 10 | 18658818197810381 | A1 | 3.0 | 63.0 | 7.0 | 6.0 |
| 11 | 20449203507642281 | A1 | 13.0 | 0.0 | 0.0 | 0.0 |
| 12 | 20795828045873027 | A1 | 3.0 | 3.0 | 3.0 | 3.0 |
| 13 | 24144092107925848 | A1 | 25.0 | 0.0 | 0.0 | 0.0 |
| 14 | 26317307137967461 | B | 5.0 | 0.0 | 0.0 | 0.0 |
| 15 | 27899413433550864 | B | 9.0 | 0.0 | 0.0 | 0.0 |
| 16 | 28534696657485531 | A2 | 11.0 | 7.0 | 6.0 | 5.0 |
| 17 | 28755862496905658 | A2 | 6.0 | 0.0 | 2.0 | 0.0 |
| 18 | 29094035245869447 | A2 | 10.0 | 10.0 | 2.0 | 1.0 |
| 19 | 31467376676344061 | B | 10.0 | 7.0 | 15.0 | 14.0 |
df_pivot_events[['count_main', 'count_offers', 'count_cart', 'count_payment']].max()
count_main 205.0 count_offers 509.0 count_cart 1100.0 count_payment 1085.0 dtype: float64
Какой_то пользователь сделал 1085 заказов за неделю, похоже на сбой
df_pivot_events[['count_main', 'count_offers', 'count_cart', 'count_payment']].mean().round(0)
count_main 16.0 count_offers 6.0 count_cart 6.0 count_payment 5.0 dtype: float64
В среднем пользователь 16 раз просматривает главную страницу, 6 раз рпедложения и корзину и оплачивает 5 заказов. Но не следует забывать про выбросы, построим диаграммы, чтобы оценить влияние выбросов на среднее.
x_values = pd.Series(range(0, len(df_pivot_events['user_id'])))
plt.figure(figsize=(17, 10))
ax1 = plt.subplot(2, 2, 1)
plt.scatter(x_values, df_pivot_events['count_main'], lw=5, alpha=0.3)
plt.title('Просмотр главного экрана', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('', fontsize=14)
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
plt.scatter(x_values, df_pivot_events['count_offers'], lw=5, alpha=0.3)
plt.title('Просмотр экрана предложений', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('', fontsize=14)
ax3 = plt.subplot(2, 2, 3)
plt.scatter(x_values, df_pivot_events['count_cart'], lw=5, alpha=0.3)
plt.title('Корзина', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('', fontsize=14)
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
plt.scatter(x_values, df_pivot_events['count_payment'], lw=5, alpha=0.3)
plt.title('Успешная оплата', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('', fontsize=14)
plt.tight_layout()
plt.show()
Нормальное количество просмотров главной страницы не более 80, Экран предложений обычно просматривают не более 50 раз, а для графиков корзины и оплаты выбросы настолько велики, что придется строить новый график, чтобы оценить нормальные значения.
df_pivot_events_mini = df_pivot_events[df_pivot_events['count_cart']<200]
x_values = pd.Series(range(0, len(df_pivot_events_mini['user_id'])))
plt.figure(figsize=(17, 10))
ax1 = plt.subplot(2, 2, 3)
plt.scatter(x_values, df_pivot_events_mini['count_cart'], lw=5, alpha=0.1)
plt.title('Корзина', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('', fontsize=14)
ax2 = plt.subplot(2, 2, 4, sharey=ax1)
plt.scatter(x_values, df_pivot_events_mini['count_payment'], lw=5, alpha=0.1)
plt.title('Успешная оплата', fontsize=16)
plt.grid()
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.ylabel('', fontsize=14)
plt.tight_layout()
plt.show()
Нормальное количество просмотров корзины не более 25, к оплате редко переходят больше 20 раз.
На всех графиках выбросы в десятки раз превышают нормальные значения, рассмотрим медианные значения.
df_pivot_events[['count_main', 'count_offers', 'count_cart', 'count_payment']].median()
count_main 11.0 count_offers 2.0 count_cart 0.0 count_payment 0.0 dtype: float64
df_pivot_events.groupby('group')[['count_main', 'count_offers', 'count_cart', 'count_payment']].mean()
| count_main | count_offers | count_cart | count_payment | |
|---|---|---|---|---|
| group | ||||
| A1 | 15.173580 | 5.947241 | 5.916230 | 4.773258 |
| A2 | 15.561306 | 6.042596 | 4.949841 | 3.973328 |
| B | 16.000789 | 6.464300 | 5.987771 | 4.767258 |
df_pivot_events.groupby('group')[['count_main', 'count_offers', 'count_cart', 'count_payment']].median()
| count_main | count_offers | count_cart | count_payment | |
|---|---|---|---|---|
| group | ||||
| A1 | 11.0 | 2.0 | 1.0 | 0.0 |
| A2 | 11.0 | 2.0 | 0.0 | 0.0 |
| B | 11.0 | 2.0 | 0.0 | 0.0 |
По группам различий в поведении пользователей не наблюдается.
В логе представлено 5 событий:
MainScreenAppear - главный экран (пользователь заходит на сайт)
OffersScreenAppear - экран предложений (просмотр товарных предложений)
CartScreenAppear - экран корзины (добавление товара в корзину)
PaymentScreenSuccessful - экран успешной оплаты (оплата заказа)
Tutorial - обучение
Представленны данные 7551 пользователей.
Распеределение пользователей по группам равномерное.
В среднем на пользователя приходится 32 события, в разрезе групп различий по среднему количеству событий на пользователя не обнаружено, колебания от 30 до 33 событий можно считать нормальными.
Среднее количество событий на пользователя было сильно искажено выбросами, превосходящими 2000 действий, поэтому при оценке нормальной пользовательской активности следует опираться на медианное значение.
Медианное число действий на пользователя равно 20 событиям, в разрезе групп различий по медианному количеству событий на пользователя не обнаружено, колебания от 19 до 20 событий можно считать нормальными.
Минимальная и максимальная дата для всех групп одинаковы, значит данные для всех групп собирались на протяжении всего срока эксперимента.
При построении графика зависимости кличества событий от даты, заметно что данных за июль очень мало по сравнению с данными за август.
Из исследования были удалены данные полученные за июль, при этом были потеряны данные 17 пользвателей из 7551, и менее 1 процента всех событий в логе. Распределение пользователей по группам также не пострадало.
В среднем пользователь 16 раз просматривает главную страницу, 6 раз рпедложения и корзину и оплачивает 5 заказов. Но не следует забывать про выбросы, построим диаграммы, чтобы оценить влияние выбросов на среднее.
Нормальное количество просмотров главной страницы не более 80, Экран предложений обычно просматривают не более 50 раз, а для графиков корзины и оплаты выбросы настолько велики, что придется строить новый график, чтобы оценить нормальные значения. Нормальное количество просмотров корзины не более 25, к оплате редко переходят больше 20 раз.
На всех графиках выбросы в десятки раз превышают нормальные значения, рассмотрим медианные значения.
Медиана для просмотров главного экрана равна 11 событиям на пользователя, на страницу товаров обычно заходят 2 раза, а вот к корзине и оплате чаще всего вообще не переходят.
df['event_name'].value_counts()
MainScreenAppear 119101 OffersScreenAppear 46808 CartScreenAppear 42668 PaymentScreenSuccessful 34118 Tutorial 1018 Name: event_name, dtype: int64
Чаще всего пользователи открывают главный экран, а реже всего пользуются обучением.
df_events = df.groupby('event_name').agg({'event_name':'count', 'user_id':'nunique'}).sort_values(by='user_id', ascending=False)
df_events['events_pers'] = round((df_events['event_name']/df_clear['event_name'].count())*100, 2)
df_events['users_pers'] = round((df_events['user_id']/df_clear['user_id'].nunique())*100, 2)
df_events.columns = ['events_count', 'users_count', 'events_pers', 'users_pers']
df_events
| events_count | users_count | events_pers | users_pers | |
|---|---|---|---|---|
| event_name | ||||
| MainScreenAppear | 119101 | 7439 | 49.44 | 98.74 |
| OffersScreenAppear | 46808 | 4613 | 19.43 | 61.23 |
| CartScreenAppear | 42668 | 3749 | 17.71 | 49.76 |
| PaymentScreenSuccessful | 34118 | 3547 | 14.16 | 47.08 |
| Tutorial | 1018 | 847 | 0.42 | 11.24 |
Половину от всех данных составляют открытия главного экрана, это действие совершают 98% пользователей (почему не 100% ведь взаимодействие с сайтом должно начинаться именно здесь? Тут либо ошибка в данных либо пользователь совершил это действие ранее, а в исследуемый период провалился например сразу в корзину, если так, это будет искажать процент удержания для шагов)
Обучением работе с сайтом пользуются около 10% пользователей.
df_events_groups = df.groupby(
['event_name','group']).agg(
{'user_id':'nunique'})
df_events_groups = df_events_groups.reset_index().sort_values(by='user_id', ascending=False)
order = df_events_groups['event_name'].drop_duplicates()
sns.catplot(y="user_id",
x="event_name",
hue="group",
order=order,
data=df_events_groups,
kind="bar",
height=7,
aspect=2.5,
legend=False)
plt.title('Частота встречаемости каждого вида события (по группам)', fontsize=18)
plt.ylabel('Количество событий', fontsize=16)
plt.xlabel('Действие', fontsize=16)
plt.grid()
plt.legend(fontsize=14, loc='upper right')
plt.xticks(rotation=45, fontsize=14)
plt.yticks(fontsize=14);
На гравике заметно постепенное уменьшение количества пользователей на каждом шаге, причем для всех групп частота встречаемости событий примерно одинакова.
MainScreenAppear - главный экран (пользователь заходит на сайт)
OffersScreenAppear - экран предложений (просмотр товарных предложений)
CartScreenAppear - экран корзины (добавление товара в корзину)
PaymentScreenSuccessful - экран успешной оплаты (оплата заказа)
Tutorial - обучение
Первые 4 действия взамосвязвны и идут последоватально, "обучение" - необязательный этап, большинству пользователей он не понадобился, его в воронку событий включать не будем.
df_events = df_events[:4]
df_events
| events_count | users_count | events_pers | users_pers | |
|---|---|---|---|---|
| event_name | ||||
| MainScreenAppear | 119101 | 7439 | 49.44 | 98.74 |
| OffersScreenAppear | 46808 | 4613 | 19.43 | 61.23 |
| CartScreenAppear | 42668 | 3749 | 17.71 | 49.76 |
| PaymentScreenSuccessful | 34118 | 3547 | 14.16 | 47.08 |
for row in df_events:
df_events['retention_from_previous_step']= ((df_events['users_count'] / df_events.shift(1)['users_count'])*100).round(2)
for row in df_events:
df_events['retention_from_start'] = 0
df_events['retention_from_start'] = ((df_events['users_count'] / 7439)*100).round(2)
df_events = df_events[['users_count','retention_from_previous_step', 'retention_from_start']]
df_events = df_events.fillna(value=100)
df_events = df_events.reset_index(drop=False)
df_events
| event_name | users_count | retention_from_previous_step | retention_from_start | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 7439 | 100.00 | 100.00 |
| 1 | OffersScreenAppear | 4613 | 62.01 | 62.01 |
| 2 | CartScreenAppear | 3749 | 81.27 | 50.40 |
| 3 | PaymentScreenSuccessful | 3547 | 94.61 | 47.68 |
В столбце 'retention_from_previous_step' удержание пользоавателей по сравнению с предыдущим шагом
В столбце 'retention_from_start' удержание пользоавателей по сравнению с открытием главного экрана(то есть началом взаимодействия с сайтом)
Больше всего пользователей уходят после просмотра главного экрана, 37,99% пользователей даже не просматривают товарные предложения, возможно они попадают на сайт случайно, и даже не собирались оформлять заказ, а может быт присутствует какая-то техническая проблема на этом этапе (вылетает приложение/сайт, что-то зависает, не работают кнопки перехода к выбору товара/поиск). Этот момент необходимо уточнить у тестировщиков или службы поддержки.
После просмотра товарных предложений в крзину переходят 81,27% пользователей, а из них 94,61% оплачивают заказ.
Из первончального числа пользователей до оплаты заказа доходят 47,68% пользователей.
Эта информация визуализирована на графиках ниже.
plt.figure(figsize=(17, 7))
plt.plot(df_events['event_name'], df_events['retention_from_start'], label='retention_from_start', lw=4)
plt.title('График удержания пользователей по шагам', fontsize=16)
plt.legend(fontsize=14)
plt.grid()
plt.xticks(rotation=45, fontsize=11)
plt.yticks(fontsize=11)
plt.xlabel('шаг', fontsize=14)
plt.ylabel('процент удержания, %', fontsize=14);
fig = px.funnel(
df_events,
x='retention_from_start',
y='event_name',
title='Воронка удержания пользователей от старта по шагам ')
fig.show()
47,68% удержания от открытия сайта до покупки кажутся прекрасным результатом, но есть нюанс. Возможно некоторые пользователи начали своё взяимодействие с сайтом раньше исследуемого периода (например положили товар в корзину еще 20 июля, а оплатили только 2 августа). рассмотрим пользователей, чье взаимодействие с сайтом в исследуемом периоде началось с просмтора главного экрана.
df_clear_pivot = df_clear.pivot_table(index='user_id', columns='event_name', values='date_time', aggfunc='min')
df_clear_pivot = df_clear_pivot.reset_index()
df_clear_pivot = df_clear_pivot[['user_id', 'MainScreenAppear', 'OffersScreenAppear', 'CartScreenAppear', 'PaymentScreenSuccessful']]
df_clear_pivot = df_clear_pivot.dropna(subset=['MainScreenAppear'])
step_1 = (~df_clear_pivot['MainScreenAppear'].isna())
step_2 = step_1 & (df_clear_pivot['OffersScreenAppear'] > df_clear_pivot['MainScreenAppear'])
step_3 = step_2 & (df_clear_pivot['CartScreenAppear'] > df_clear_pivot['OffersScreenAppear'])
step_4 = step_3 & (df_clear_pivot['PaymentScreenSuccessful'] > df_clear_pivot['CartScreenAppear'])
MainScreenAppear = df_clear_pivot[step_1].shape[0]
OffersScreenAppear = df_clear_pivot[step_2].shape[0]
CartScreenAppear = df_clear_pivot[step_3].shape[0]
PaymentScreenSuccessful = df_clear_pivot[step_4].shape[0]
df_funnel = pd.DataFrame( columns = ['event_name', 'users_count'], data = [
['MainScreenAppear',MainScreenAppear],
['OffersScreenAppear',OffersScreenAppear],
['CartScreenAppear',CartScreenAppear],
['PaymentScreenSuccessful',PaymentScreenSuccessful]
])
df_funnel
| event_name | users_count | |
|---|---|---|
| 0 | MainScreenAppear | 7419 |
| 1 | OffersScreenAppear | 4201 |
| 2 | CartScreenAppear | 1767 |
| 3 | PaymentScreenSuccessful | 454 |
Оставили только тех пользователей, котрые прошли полный путь. Теперь проверим удержание по ним.
for row in df_funnel:
df_funnel['retention_from_previous_step']= ((df_funnel['users_count'] / df_funnel.shift(1)['users_count'])*100).round(2)
for row in df_events:
df_funnel['retention_from_start'] = 0
df_funnel['retention_from_start'] = ((df_funnel['users_count'] / 7419)*100).round(2)
df_funnel = df_funnel[['event_name', 'users_count','retention_from_previous_step', 'retention_from_start']]
df_funnel = df_funnel.fillna(value=100)
df_funnel
| event_name | users_count | retention_from_previous_step | retention_from_start | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 7419 | 100.00 | 100.00 |
| 1 | OffersScreenAppear | 4201 | 56.62 | 56.62 |
| 2 | CartScreenAppear | 1767 | 42.06 | 23.82 |
| 3 | PaymentScreenSuccessful | 454 | 25.69 | 6.12 |
plt.figure(figsize=(17, 7))
plt.plot(df_funnel['event_name'], df_funnel['retention_from_start'], label='retention_from_start', lw=4)
plt.title('График удержания пользователей по шагам', fontsize=16)
plt.legend(fontsize=14)
plt.grid()
plt.xticks(rotation=45, fontsize=11)
plt.yticks(fontsize=11)
plt.xlabel('шаг', fontsize=14)
plt.ylabel('процент удержания, %', fontsize=14);
fig = px.funnel(df_funnel, x='retention_from_start', y='event_name', title='Воронка удержания пользователей от старта по шагам')
fig.show()
Теперь ситуация выглядит совсем по-другому, от главной страницы до оплаты доходит только 6,12% пользователей.
По очищенным данным видно что с каждым шагом остается все меньший процент пользователей, от главной страницы к товарам переходит 56,62%, от товаров к корзине 42,06%, а оплачивают заказ только 25,69% из тех кто положил его в корзину.
Возможно тут дело в "медлительности" покупателей, у 23,82% пользователей товары лежат в корзине, может быть он оплатит их позже.
Чаще всего пользователи открывают главный экран, на втором месте страница товаров, на третьем страница корзины, далее страница успешной оплаты, а реже всего пользуются обучением.
Первые 4 действия взамосвязаны и идут последоватально, "обучение" - необязательный этап, большинству пользователей он не понадобился, его в воронку событий включать не будем.
Половину от всех данных составляют открытия главного экрана, это действие совершают 98% пользователей (почему не 100% ведь взаимодействие с сайтом должно начинаться именно здесь? Тут либо ошибка в данных либо пользователь совершил это действие ранее, а в исследуемый период провалился например сразу в корзину, если так, это будет искажать процент удержания для шагов)
Обучением работе с сайтом пользуются около 10% пользователей.
На графике частоты встречаемости каждого вида события по группам заметно постепенное уменьшение количества пользователей на каждом шаге, причем для всех групп частота встречаемости событий примерно одинакова.
Больше всего пользователей уходят после просмотра главного экрана, 39% пользователей даже не просматривают товарные предложения, возможно они попадают на сайт случайно, и даже не собирались оформлять заказ, а может быт присутствует какая-то техническая проблема на этом этапе (вылетает приложение/сайт, что-то зависает, не работают кнопки перехода к выбору товара/поиск). Этот момент необходимо уточнить у тестировщиков или службы поддержки.
После просмотра товарных предложений в крзину переходят 81% пользователей, а из них 94% оплачивают заказ.
Из первончального числа пользователей до оплаты заказа доходят 47% пользователей.
47% удержания от открытия сайта до покупки кажутся прекрасным результатом, но есть нюанс. Возможно некоторые пользователи начали своё взяимодействие с сайтом раньше исследуемого периода (например положили товар в корзину еще 20 июля, а оплатили только 2 августа). рассмотрим пользователей, чье взаимодействие с сайтом в исследуемом периоде началось с просмтора главного экрана.
Если рассмотреть данные только тех пользователей, кто начал взаимодействие с сайтом с открытия главной страницы, ситуация выглядит совсем по-другому, от главной страницы до оплаты доходит только 6% пользователей.
По очищенным данным видно что с каждым шагом остается все меньший процент пользователей, от главной страницы к товарам переходит 56%, от товаров к корзине 42%, а оплачивают заказ только 25% из тех кто положил его в корзину.
Возможно тут дело в "медлительности" покупателей, у 23% пользователей товары лежат в корзине, может быть они оплатят их позже.
Очевидно одной недели экперимента мало, похоже что путь пользователя от начала работы с сайтом до оформления заказа часто занимает больше времени.
df_clear['user_id'].nunique()
7534
users_group = df_clear.groupby('group')['user_id'].nunique()
users_group['A1+A2'] = users_group['A1'] + users_group['A2']
users_group
group A1 2484 A2 2513 B 2537 A1+A2 4997 Name: user_id, dtype: int64
df_A1_clear = df.query('group == "A1"')
df_A2_clear = df.query('group == "A2"')
df_B_clear = df.query('group == "B"')
print('Процент пользователей из группы A1:', round(df_A1_clear['user_id'].nunique()/df_clear['user_id'].nunique(),2), '%')
print()
print('Процент пользователей из группы A2:', round(df_A2_clear['user_id'].nunique()/df_clear['user_id'].nunique(),2), '%')
print()
print('Процент пользователей из группы B:', round(df_B_clear['user_id'].nunique()/df_clear['user_id'].nunique(),2), '%')
Процент пользователей из группы A1: 0.33 % Процент пользователей из группы A2: 0.33 % Процент пользователей из группы B: 0.34 %
Пользователи равномерно распределены по группам.
df_test = df_clear[df_clear['event_name' ]!= 'Tutorial'].pivot_table(index='event_name',columns = 'group', values='user_id',aggfunc='nunique')
df_test = df_test.reset_index()
df_test.columns =['event_name', 'A1', 'A2', 'B']
df_test['A1+A2'] = df_test['A1'] + df_test['A2']
df_test['all'] = df_test['A1+A2'] + df_test['B']
df_test['A1_pers'] = ((df_test['A1']/users_group['A1'])*100).round(2)
df_test['A2_pers'] = ((df_test['A2']/users_group['A2'])*100).round(2)
df_test['B_pers'] = ((df_test['B']/users_group['B'])*100).round(2)
df_test['A1+A2_pers'] = ((df_test['A1+A2']/(df_A1_clear['user_id'].nunique() + df_A2_clear['user_id'].nunique()))*100).round(2)
df_test.sort_values(by='A1', ascending=False)
| event_name | A1 | A2 | B | A1+A2 | all | A1_pers | A2_pers | B_pers | A1+A2_pers | |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | MainScreenAppear | 2450 | 2476 | 2493 | 4926 | 7419 | 98.63 | 98.53 | 98.27 | 98.34 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 | 3062 | 4593 | 62.08 | 60.49 | 60.35 | 61.13 |
| 0 | CartScreenAppear | 1266 | 1238 | 1230 | 2504 | 3734 | 50.97 | 49.26 | 48.48 | 49.99 |
| 3 | PaymentScreenSuccessful | 1200 | 1158 | 1181 | 2358 | 3539 | 48.31 | 46.08 | 46.55 | 47.08 |
Нулевая гипотеза: между поведением пользователей в исследуемых группах нет различий
Альтернативная гипотеза: между поведением пользователей в исследуемых группах есть различия
def test(group_1, group_2, alpha):
alpha = 1-(1-alpha)**(1/16)
for i in df_test.index:
p1 = df_test[group_1][i] / users_group[group_1]
# пропорция успехов во второй группе:
p2 = df_test[group_2][i] / users_group[group_2]
# пропорция успехов в комбинированном датасете:
p_combined = ((df_test[group_1][i] + df_test[group_2][i]) /
(users_group[group_1] + users_group[group_2]))
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) *
(1/users_group[group_1] + 1/users_group[group_2]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('Результат по шагу', df_test['event_name'][i], ':')
print('p-значение =', p_value)
if (p_value < alpha):
print("Отвергаем нулевую гипотезу: между исследуемыми группами есть различия")
else:
print("Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий")
print('')
test('A1', 'A2', 0.01)
Результат по шагу CartScreenAppear : p-значение = 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу MainScreenAppear : p-значение = 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу OffersScreenAppear : p-значение = 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу PaymentScreenSuccessful : p-значение = 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий
test('A1', 'B', 0.01)
Результат по шагу CartScreenAppear : p-значение = 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу MainScreenAppear : p-значение = 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу OffersScreenAppear : p-значение = 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу PaymentScreenSuccessful : p-значение = 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий
test('A2', 'B', 0.01)
Результат по шагу CartScreenAppear : p-значение = 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу MainScreenAppear : p-значение = 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу OffersScreenAppear : p-значение = 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу PaymentScreenSuccessful : p-значение = 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий
test('A1+A2', 'B', 0.01)
Результат по шагу CartScreenAppear : p-значение = 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу MainScreenAppear : p-значение = 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу OffersScreenAppear : p-значение = 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий Результат по шагу PaymentScreenSuccessful : p-значение = 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, между исследуемыми группами нет различий
После очистки в логе остались данные 7534 пользователей, они равномерно распределены между группами.
При проверке гипотезы на двух контрольных группах отличий не было выявлено, можно считать, что разбиение на группы работает корректно.
Нулевую гипотезу о присутствии различий между контрольными группами и тестовой не удалось отвергнуть ни в одном тесте.
В таблице df пять столбцов :
event_name - название события
user_id - идентификатор пользователя
date_time - дата и время совершения действия
group - группа эксперимента
date - дата совершения действия (без времени)
Таблица содержит данные о 243713 действиях на сайте.
Пропусков в таблице не было, а 413 строк-дубликатов удалены.
Изменены названия столбцов, названия групп эксперимета, заменен тип данных в столбце с датой и временем.
В логе представлено 5 событий:
MainScreenAppear - главный экран (пользователь заходит на сайт)
OffersScreenAppear - экран предложений (просмотр товарных предложений)
CartScreenAppear - экран корзины (добавление товара в корзину)
PaymentScreenSuccessful - экран успешной оплаты (оплата заказа)
Tutorial - обучение
Представленны данные 7551 пользователей.
Распеределение пользователей по группам равномерное.
В среднем на пользователя приходится 32 события, в разрезе групп различий по среднему количеству событий на пользователя не обнаружено, колебания от 30 до 33 событий можно считать нормальными.
Среднее количество событий на пользователя было сильно искажено выбросами, превосходящими 2000 действий, поэтому при оценке нормальной пользовательской активности следует опираться на медианное значение.
Медианное число действий на пользователя равно 20 событиям, в разрезе групп различий по медианному количеству событий на пользователя не обнаружено, колебания от 19 до 20 событий можно считать нормальными.
Минимальная и максимальная дата для всех групп одинаковы, значит данные для всех групп собирались на протяжении всего срока эксперимента.
При построении графика зависимости кличества событий от даты, заметно что данных за июль очень мало по сравнению с данными за август.
Из исследования были удалены данные полученные за июль, при этом были потеряны данные 17 пользвателей из 7551, и менее 1 процента всех событий в логе. Распределение пользователей по группам также не пострадало.
В среднем пользователь 16 раз просматривает главную страницу, 6 раз рпедложения и корзину и оплачивает 5 заказов. Но не следует забывать про выбросы, построим диаграммы, чтобы оценить влияние выбросов на среднее.
Нормальное количество просмотров главной страницы не более 80, Экран предложений обычно просматривают не более 50 раз, а для графиков корзины и оплаты выбросы настолько велики, что придется строить новый график, чтобы оценить нормальные значения. Нормальное количество просмотров корзины не более 25, к оплате редко переходят больше 20 раз.
На всех графиках выбросы в десятки раз превышают нормальные значения.
Медиана для просмотров главного экрана равна 11 событиям на пользователя, на страницу товаров обычно заходят 2 раза, а вот к корзине и оплате чаще всего вообще не переходят.
Чаще всего пользователи открывают главный экран, на втором месте страница товаров, на третьем страница корзины, далее страница успешной оплаты, а реже всего пользуются обучением.
Первые 4 действия взамосвязаны и идут последоватально, "обучение" - необязательный этап, большинству пользователей он не понадобился, его в воронку событий включать не будем.
Половину от всех данных составляют открытия главного экрана, это действие совершают 98% пользователей (почему не 100% ведь взаимодействие с сайтом должно начинаться именно здесь? Тут либо ошибка в данных либо пользователь совершил это действие ранее, а в исследуемый период провалился например сразу в корзину, если так, это будет искажать процент удержания для шагов)
Обучением работе с сайтом пользуются около 10% пользователей.
На графике частоты встречаемости каждого вида события по группам заметно постепенное уменьшение количества пользователей на каждом шаге, причем для всех групп частота встречаемости событий примерно одинакова.
Больше всего пользователей уходят после просмотра главного экрана, 39% пользователей даже не просматривают товарные предложения, возможно они попадают на сайт случайно, и даже не собирались оформлять заказ, а может быт присутствует какая-то техническая проблема на этом этапе (вылетает приложение/сайт, что-то зависает, не работают кнопки перехода к выбору товара/поиск). Этот момент необходимо уточнить у тестировщиков или службы поддержки.
После просмотра товарных предложений в крзину переходят 81% пользователей, а из них 94% оплачивают заказ.
Из первончального числа пользователей до оплаты заказа доходят 47% пользователей.
47% удержания от открытия сайта до покупки кажутся прекрасным результатом, но есть нюанс. Возможно некоторые пользователи начали своё взяимодействие с сайтом раньше исследуемого периода (например положили товар в корзину еще 20 июля, а оплатили только 2 августа). рассмотрим пользователей, чье взаимодействие с сайтом в исследуемом периоде началось с просмтора главного экрана.
Если рассмотреть данные только тех пользователей, кто начал взаимодействие с сайтом с открытия главной страницы, ситуация выглядит совсем по-другому, от главной страницы до оплаты доходит только 6% пользователей.
По очищенным данным видно что с каждым шагом остается все меньший процент пользователей, от главной страницы к товарам переходит 56%, от товаров к корзине 42%, а оплачивают заказ только 25% из тех кто положил его в корзину.
Возможно тут дело в "медлительности" покупателей, у 23% пользователей товары лежат в корзине, может быть они оплатят их позже.
Очевидно одной недели экперимента мало, похоже что путь пользователя от начала работы с сайтом до оформления заказа часто занимает больше времени.
После очистки в логе остались данные 7534 пользователей, они равномерно распределены между группами.
При проверке гипотезы на двух контрольных группах отличий не было выявлено, можно считать, что разбиение на группы работает корректно.
Нулевую гипотезу о присутствии различий между контрольными группами и тестовой не удалось отвергнуть ни в одном тесте.
За неделю проведения эксперимента удалось определить, что разбиение на группы работает корректно, в группах примерно равное количество пользователей, и попав в одну группу пользователь остается в ней на протяжении всего эксперимента.
Однако для оценки изменений в поведении пользователей тестовой группы нужно больше времени. Также для исследования будет полезно оценить суммы заказов. Есть вероятность что изменение шрифтов на сайте побудит пользователей заказывать чаще, но на меньшие суммы.
Для доказтельства или опровержения экономической эффективности изменений следует:
Предоставить данные о стоимости покупок клиентов, для расчета среднего чека и выручки (как для уже представленных в логе клиентов, так и для будущих)
Если существует и мобильная и десктопная версия приложения, предоставить информацию о типе устройства пользователей
Продлить эксперимент до конца августа